﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FinanSoft.Entities;
using FinanSoft.Utilities;
using FinanSoft.DataLayer;

namespace FinanSoft.BusinessLayer
{
    class AccountingEntryManager
    {
        #region Fields
        private bool _isError;
        private String _errorMessage;
        private bool _definiteError;
        private static AccountingEntryManager _instance;
        #endregion

        #region Constructors
        /// <summary>
        /// Constructor privado de la clase para permitir la implementacion del patron singleton
        /// </summary>
        private AccountingEntryManager()
        {

        }
        #endregion        

        #region Properties
        /// <summary>
        /// Boolean que determina si ha ocurrido algún error en la ejecución de los métodos de la clase
        /// </summary>
        public bool IsError
        {
            get
            {
                bool tempError = _isError;
                if (!_definiteError)
                    _isError = false;
                return tempError;
            }
        }

        /// <summary>
        ///  Boolean que determina si ha ocurrido algún error terminal en la ejecución de los métodos de la clase
        /// </summary>
        public String ErrorMessage
        {
            get
            {
                String tempError = _errorMessage;
                if (!_definiteError)
                {
                    _isError = false;
                    _errorMessage = "";
                }
                return tempError;
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Metodo que obtienen la instancia existente de la clase. Si esta aun no ha sido instanciada, la instancia.
        /// </summary>
        public static AccountingEntryManager GetInstance()
        {
            if (_instance == null)
                _instance = new AccountingEntryManager();
            return _instance;
        }

        /// <summary>
        /// Se encarga de construir un nuevo asiento con la información proporcionada, sin incluir la lista de detalles 
        /// (par cuenta-monto con tipo de movimiento). Esta debe ser agregada despues, par por par o toda completa. Para agregarla par
        /// por par se debe usar el metodo AddDetail.
        /// </summary>
        /// <param name="docTypeId">El id del tipo de documento al que pertenece el asiento, representado por un string</param>
        /// <param name="docDate"> La fecha de documento del asiento</param>
        /// <param name="accDate"> La fecha de contabilizacion del asiento</param>
        /// <param name="ref1"> Campo de referencia del asiento</param>
        /// <param name="ref2"> Campo de referencia del asiento</param>
        /// <param name="ref3"> Campo de referencia del asiento</param>
        /// <param name="details"> Lista de detalles del asiento. Es una lista de pares cuenta - monto, cada una con el tipo de crecimiento
        /// que aplica. Cada par debe construirse previamente a la creación del asiento usando el método BuildAccountingEntryDetail en esta misma clase </param>
        /// <returns>El nuevo asiento creado</returns>
        public AccountingEntry ConstructAccountingEntryNoDetails(string docTypeId, DateTime docDate, DateTime accDate, int seasonId, string ref1, string ref2, string ref3)
        {
            AccountingEntry newEntry;
            User currentuser;
            Season entrySeason;
            DocType docType;

            currentuser = UserManager.GetInstance().GetCurrentUser();
            entrySeason = PeriodManager.GetInstance().GetSeason(seasonId);
            docType = DocumentTypeManager.GetInstance().GetType(docTypeId);
            newEntry = new AccountingEntry(Utilities.Constants.INVALID_ID, entrySeason, docType, currentuser, accDate, docDate, ref1, ref2, ref3, new List<AccountingEntryDetail>());

            return newEntry;
        }

        /// <summary>
        /// Se encarga de construir un nuevo asiento con la información proporcionada.
        /// </summary>
        /// <param name="docTypeId">El id del tipo de documento al que pertenece el asiento, representado por un string</param>
        /// <param name="docDate"> La fecha de documento del asiento</param>
        /// <param name="accDate"> La fecha de contabilizacion del asiento</param>
        /// <param name="ref1"> Campo de referencia del asiento</param>
        /// <param name="ref2"> Campo de referencia del asiento</param>
        /// <param name="ref3"> Campo de referencia del asiento</param>
        /// <param name="details"> Lista de detalles del asiento. Es una lista de pares cuenta - monto, cada una con el tipo de crecimiento
        /// que aplica. Cada par debe construirse previamente a la creación del asiento usando el método BuildAccountingEntryDetail en esta misma clase </param>
        /// <returns>El nuevo asiento creado</returns>
        public AccountingEntry ConstructAccountingEntry(string docTypeId, DateTime docDate, DateTime accDate, int seasonId, string ref1, string ref2, string ref3, List<AccountingEntryDetail> details)
        {
            AccountingEntry newEntry;
            User currentuser;
            Season entrySeason;
            DocType docType;

            currentuser = UserManager.GetInstance().GetCurrentUser();
            entrySeason = PeriodManager.GetInstance().GetSeason(seasonId);
            docType = DocumentTypeManager.GetInstance().GetType(docTypeId);
            newEntry = new AccountingEntry(Utilities.Constants.INVALID_ID, entrySeason, docType, currentuser, accDate, docDate, ref1, ref2, ref3, details);

            return newEntry;
        }

        /// <summary>
        /// Se encarga de construir un nuevo asiento con la información proporcionada y solicitar su registro en la base de datos
        /// </summary>
        /// <param name="docTypeId">El id del tipo de documento al que pertenece el asiento, representado por un string</param>
        /// <param name="docDate"> La fecha de documento del asiento</param>
        /// <param name="accDate"> La fecha de contabilizacion del asiento</param>
        /// <param name="ref1"> Campo de referencia del asiento</param>
        /// <param name="ref2"> Campo de referencia del asiento</param>
        /// <param name="ref3"> Campo de referencia del asiento</param>
        /// <param name="details"> Lista de detalles del asiento. Es una lista de pares cuenta - monto, cada una con el tipo de crecimiento
        /// que aplica. Cada par debe construirse previamente a la creación del asiento usando el método BuildAccountingEntryDetail en esta misma clase </param>
        /// <returns>Un valor de verdad correspondiente al exito del registro</returns>
        public bool CreateAndRecordAccountingEntry(string docTypeId, DateTime docDate, DateTime accDate, int seasonId, string ref1, string ref2, string ref3, List<AccountingEntryDetail> details)
        {
            AccountingEntry newEntry;
            User currentuser;
            Season entrySeason;
            DocType docType;   

            currentuser = UserManager.GetInstance().GetCurrentUser();
            entrySeason = PeriodManager.GetInstance().GetSeason(seasonId);
            docType = DocumentTypeManager.GetInstance().GetType(docTypeId);
            newEntry = new AccountingEntry(Utilities.Constants.INVALID_ID, entrySeason, docType, currentuser, accDate, docDate, ref1, ref2, ref3, details);            

            return RecordAccountingEntry(newEntry);
        }

        /// <summary>
        /// Verifica que el asiento sea un asiento valido para ser registrado y solicita su registro en la base de datos.
        /// </summary>
        /// <param name="entry"> Asiento del que se verificara su validez y sera registrado en la base de datos</param>
        /// <returns>Un valor de verdad correspondiente al exito del registro</returns>
        public bool RecordAccountingEntry(AccountingEntry entry)
        {
            bool wasRecorded;
            bool isBalanced;
            bool validAccounts;

            //Verifica que haya movimientos asociados al asiento
            if (entry.EntryDetails.Count == 0)
            {
                _isError = true;
                _errorMessage = FinanSoft.Utilities.Constants.ERROR_NO_DETAILS_ASSOCIATED;
                return false; 
            }

            //Verifica que los creditos y debitos del asiento esten balanceados
            isBalanced = this.CheckCreditsAndDebits(entry);

            //Verifica que las cuentas sean validas
            validAccounts = true;//this.CheckAccounts(entry);

            if (!isBalanced)
            {
                _isError = true;
                _errorMessage = FinanSoft.Utilities.Constants.ERROR_ACCOUNTING_ENTRY_UNBALANCED;
                return false;
            }

            if (!validAccounts)
            {
                _isError = true;
                _errorMessage = FinanSoft.Utilities.Constants.ERROR_ACCOUNTING_ENTRY_INVALID_ACCOUNTS;
                return false;
            }

            wasRecorded = AccountingEntryDataManager.GetInstance().AddAccountingEntry(entry);

            if (!wasRecorded)
            {
                _isError = true;
                _errorMessage = FinanSoft.Utilities.Constants.ERROR_ACCOUNTING_ENTRY_REGISTRY;
                return false;
            }
            else
            {
                foreach (AccountingEntryDetail entryLine in entry.EntryDetails)
                {
                    if (entryLine.AuxiliarAccountId != Constants.INVALID_ID)
                        if (!AccountingEntryDataManager.GetInstance().UpdateAuxiliarAccountBalance(entryLine.AuxiliarAccountId, entryLine.LocAmmount, entryLine.Type))
                        {
                            _isError = true;
                            _errorMessage = FinanSoft.Utilities.Constants.ERROR_AUXILIAR_ACCOUNT_UPDATE_FAILED;
                            return false;
                        }
                        else
                        {
                            AuxiliarAccount modifiedAccount = AuxiliaryAccountManager.GetInstance().GetAuxiliarAccount(entryLine.AuxiliarAccountId);
                            modifiedAccount.AddAmount(entryLine.LocAmmount / modifiedAccount.AccountCurrency.ToLocalCurrency, entryLine.Type);
                        }

                    CatalogManager.GetInstance().GetAccount(entryLine.Account.AccountID).UpdateAmount(entryLine.Type == Constants.AccountingEntryType.debit, entryLine.SysAmmount, entryLine.LocAmmount);
                }
            }

            return wasRecorded;
        }

        /// <summary>
        /// Verifica que el asiento que se registra este balanceado
        /// </summary>
        /// <param name="entry"> Asiento del que se verificaran los creditos y debitos</param>
        /// <returns> Verdadero si la suma de los creditos y debitos es 0</returns>
        private bool CheckCreditsAndDebits(AccountingEntry entry)
        {
            bool balanced;
            double ammount;

            ammount = 0;
            foreach (AccountingEntryDetail detail in entry.EntryDetails)
            {
                if (detail.Type == Utilities.Constants.AccountingEntryType.debit)
                    ammount -= detail.SysAmmount;
                else
                    ammount += detail.SysAmmount;
            }

            balanced = ammount == 0;
            return balanced;
        }

        /// <summary>
        /// Verifica que no existan cuentas repetidas dentro de los movimientos del asiento
        /// </summary>
        /// <param name="entry"></param>
        /// <returns></returns>
        private bool CheckAccounts(AccountingEntry entry)
        {
            bool valid;
            List<Account> existingAccounts;

            existingAccounts = new List<Account>();
            valid = true;

            foreach (AccountingEntryDetail detail in entry.EntryDetails)
            {
                if (existingAccounts.Contains(detail.Account))
                {
                    valid = false;
                    return valid;
                }
                else
                    existingAccounts.Add(detail.Account);
            }

            return valid;
        }        

        /// <summary>
        /// Construye un objeto detalle, que consisten en un par cuenta - monto y el tipo de crecimiento y que documentan los movimientos del asiento.
        /// </summary>
        /// <param name="accId"> Numero de identificacion de la cuenta referenciada en el asiento</param>
        /// <param name="sysAmmount"> Cantidad de dinero en moneda del systema por la que se modifica la cuenta</param>
        /// <param name="type"> Tipo de crecimiento de la cuenta para este movimiento</param>
        /// <param name="locAmmount"> Cantidad de dinero en moneda del systema por la que se modifica la cuenta</param>
        /// <param name="idCuentaAuxiliar">Id de la cuenta auxiliar del detalle. Si el detalle no tiene cuenta auxiliar debe ingresar Constants.INVALID_ID</param>
        /// <returns></returns>
        public AccountingEntryDetail BuildAccountingEntryDetail(int accId, double sysAmmount, double locAmmount, Utilities.Constants.AccountingEntryType type, int idCuentaAuxiliar)
        {
            AccountingEntryDetail newDetail;
            FinanSoft.Entities.Account entryAccount;

            entryAccount = CatalogManager.GetInstance().GetAccount(accId);
            newDetail = new AccountingEntryDetail(Constants.INVALID_ID, entryAccount, type, sysAmmount, locAmmount);
            newDetail.AuxiliarAccountId = idCuentaAuxiliar;
            return newDetail; 
        }

        #endregion
    }
}
